#!/usr/bin/env python
#
# pdp-helper.py
#
# A helper script to update the CIP maintained package list.
# The main use cases are:
# (1) add-proposal: Register the accepted package(s) to the list
# (2) show: Check the package information registered in the list
# Also, this script provides other commands to modify the list.
#
# Examples:
#   $ ./pdp-helper.py add-proposal proposal.yml
#   $ ./pdp-helper.py buster show libssl1.1
#
# Copyright (c) 2019 TOSHIBA Corporation
#
# SPDX-License-Identifier: Apache-2.0
#

import common
import sys
import optparse
import check_deps
import get_pkg_depends as gpd

is_src_pkg = False
Q1 = "Do you want to replace with the proposal information?"


def add_src_pkg(pdp_info_obj, pkg_list, apt):
    """
    add source package information to pdp info object
    :param pdp_info_obj:
    :param pkg_list:
    :param apt:
    :return: None
    """
    for src_pkg_name in pkg_list:
        print("\nAdding source package: " + src_pkg_name)
        sp_name, sp_ver, sp_bp_list = apt.apt_cache_get_src_info(src_pkg_name)
        if sp_name == src_pkg_name:
            add_bin_pkg(pdp_info_obj, sp_bp_list, apt)
        else:
            print(common.ERROR_TAG + "Not valid source package")


def add_bin_pkg(pdp_info_obj, pkg_list, apt):
    """
    Add binary packages information to the pdp info object
    :param pdp_info_obj:
    :param pkg_list:
    :param apt:
    :return:
    """
    added_pkg_list = list()
    for pkg_name in pkg_list:
        print("\nAdding binary package: " + pkg_name)
        # check if package name already exist
        if pdp_info_obj.is_bin_pkg_exist(pkg_name):
            print(common.INFO_TAG + "package is already present")
            continue

        sp_name, sp_ver, sp_bin_list = apt.apt_cache_get_src_info(pkg_name)
        if not sp_name or not sp_ver or not sp_bin_list:
            print(common.ERROR_TAG + "not proper binary package, source information fetch failed!!!")
            continue

        if pkg_name not in sp_bin_list:
            print(common.ERROR_TAG + "Not valid binary package!!!")
            continue

        pdp_pkg_data = common.PDPInfo.SrcPkgData()
        pdp_pkg_data.src_pkg_name = sp_name
        bin_pkg_data = common.PDPInfo.BinPkgData()
        bin_pkg_data.bin_pkg_depends = gpd.get_pkg_depends(pkg_name, apt)
        pdp_pkg_data.bin_pkg_data_dict = {pkg_name: bin_pkg_data}
        pdp_info_obj.update_src_pkg_info(pdp_pkg_data)

        added_pkg_list.append(pkg_name)
        print("Done")

    pdp_info_obj.dump_pdp()
    show_bin_pkg(pdp_info_obj, added_pkg_list)


def remove_src_pkg(pdp_info_obj, pkg_list):
    """
    remove source package information from pdp_info file
    :param pdp_info_obj:
    :param pkg_list:
    :return:
    """
    for src_pkg_name in pkg_list:
        print("\nRemove source package: " + src_pkg_name)
        if pdp_info_obj.remove_src_pkg(src_pkg_name):
            pdp_info_obj.dump_pdp()
            print("Done")
        else:
            print("Not found package in the PDP file")


def remove_bin_pkg(pdp_info_obj, pkg_list):
    """
    remove binary package information from pdp_info file
    :param pdp_info_obj:
    :param pkg_list:
    :return:
    """
    for pkg_name in pkg_list:
        print("\nRemove binary package: " + pkg_name)
        if pdp_info_obj.remove_bin_pkg(pkg_name):
            pdp_info_obj.dump_pdp()
            print("Done")
        else:
            print("Not found package in the PDP file")


def show_src_pkg(pdp_info_obj, pkg_list):
    """
    Display source package information present in pdp_info file
    :param pdp_info_obj:
    :param pkg_list:
    :return: None
    """
    is_asterisk_msg = False
    for src_pkg_name in pkg_list:
        if pdp_info_obj.is_src_pkg_exist(src_pkg_name):
            pdp_pkg_info = pdp_info_obj.get_src_pkg_info(src_pkg_name)
            print("Source Package Name: " + pdp_pkg_info.src_pkg_name)
            print("Binary packages:")
            for bin_pkg_name, pkg_info in pdp_pkg_info.bin_pkg_data_dict.items():
                print("\t - " + bin_pkg_name)
                if pkg_info.bin_pkg_depends is not None and len(pkg_info.bin_pkg_depends):
                    print("\t\tDepends: ")
                    for dep in pkg_info.bin_pkg_depends:
                        if dep.startswith('<') and dep.partition('>:')[2]:
                            pkg = dep.partition('>:')[2]
                        else:
                            pkg = dep
                        if pdp_info_obj.is_bin_pkg_exist(pkg):
                            print("\t\t\t " + dep + common.IP_ASTERISK_COLOR + "*" + common.INPUT_TXT_COLOR_POST)
                            is_asterisk_msg = True
                        else:
                            print("\t\t\t " + dep)
        else:
            print("Not found package in the list: " + src_pkg_name)
    if is_asterisk_msg:
        print(common.IP_ASTERISK_COLOR + "* indicates the package is included in final pkglist_" +
              pdp_info_obj.codename + ".yml file" + common.INPUT_TXT_COLOR_POST)


def show_bin_pkg(pdp_info_obj, pkg_list):
    """
    Display the binary package information present in pdp_info file
    :param pdp_info_obj:
    :param pkg_list:
    :return: None
    """
    is_asterisk_msg = False
    for pkg_name in pkg_list:
        if pdp_info_obj.is_bin_pkg_exist(pkg_name):
            pdp_pkg_info = pdp_info_obj.get_bin_pkg_info(pkg_name)
            print("binary_package: " + pkg_name)
            print("Source Package Name: " + pdp_pkg_info.src_pkg_name)
            bin_pkg_info = pdp_pkg_info.bin_pkg_data_dict[pkg_name]
            if bin_pkg_info.bin_pkg_depends is not None and len(bin_pkg_info.bin_pkg_depends):
                print("Depends: ")
                for dep in bin_pkg_info.bin_pkg_depends:
                    if dep.startswith('<') and dep.partition('>:')[2]:
                        dep = dep.partition('>:')[2]
                    if pdp_info_obj.is_bin_pkg_exist(dep):
                        print("\t " + dep + common.IP_ASTERISK_COLOR + "*" + common.INPUT_TXT_COLOR_POST)
                        is_asterisk_msg = True
                    else:
                        print("\t " + dep)
        else:
            print("Not found package in the list: " + pkg_name)
    if is_asterisk_msg:
        print(common.IP_ASTERISK_COLOR + "* indicates the package is included in final pkglist_" +
              pdp_info_obj.codename + ".yml file" + common.INPUT_TXT_COLOR_POST)


def check_nd_print(pdp_info, prop_info, pre_txt=""):
    if pdp_info == prop_info:
        common.print_nrml_text(pre_txt + prop_info)
    elif pdp_info == "" and prop_info:
        common.print_add_text(pre_txt + prop_info)
    elif prop_info == "" and pdp_info:
        common.print_rmv_text(pre_txt + pdp_info)
    else:
        common.print_rmv_text(pre_txt + pdp_info)
        common.print_add_text(pre_txt + prop_info)


def validate_proposal(apt, pdp_info_obj, proposal_info):
    """
    validate the proposal information by checking the existence of source package information in pdp_info
    and print the differences
    :param apt:
    :param pdp_info_obj:
    :param proposal_info:
    :return: None
    """
    for sp, prop_src_pkg_info in proposal_info.proposed_src_pkgs.items():
        # validating the source packages in proposal file are present in the pdp_info file or not
        pdp_src_pkg_info = pdp_info_obj.get_src_pkg_info(sp)
        print('')
        if pdp_src_pkg_info:
            # if present, checking all contents are matching
            check_nd_print(sp, sp, "")
            check_nd_print("", "", " " + common.PROPOSER_KEY + ": ")
            for proposer in pdp_src_pkg_info.proposer:
                check_nd_print(proposer, proposer, "\t - ")
            if proposal_info.proposer_name not in pdp_src_pkg_info.proposer:
                check_nd_print("", proposal_info.proposer_name, "\t - ")
            check_nd_print(pdp_src_pkg_info.pdp_version, proposal_info.pdp_revision, " " + common.PDP_REVISION_KEY + ": ")
            check_nd_print("", "", " " + common.BIN_PKGS_KEY + ": ")

            # Prepare a final binary package data by taking info from proposal.
            final_bin_pkg_dict = dict()
            for pdp_bp, pdp_bp_info in pdp_src_pkg_info.bin_pkg_data_dict.items():
                bp_data = common.PDPInfo.BinPkgData()
                # checking all the binary packages in pdp_info are present in proposal file or not
                if pdp_bp in prop_src_pkg_info.bin_pkg_dict:
                    # if present check the contents in binary package are same or not
                    check_nd_print(pdp_bp, pdp_bp, "\t-")
                    pdp_dp_list = pdp_src_pkg_info.bin_pkg_data_dict[pdp_bp].bin_pkg_depends
                    prop_dp_list = prop_src_pkg_info.bin_pkg_dict[pdp_bp]
                    if len(pdp_dp_list) == len(prop_dp_list):
                        check_nd_print("", "", "\t  " + common.DEPENDS_KEY + ":")
                        for i in range(len(pdp_dp_list)):
                            check_nd_print(pdp_dp_list[i], prop_dp_list[i], "\t\t-")
                    else:
                        print("dependency list count is not matching, some thing wrong")
                        print("PDP info: " + pdp_dp_list)
                        print("Proposal info: " + prop_dp_list)
                    bp_data.bin_pkg_depends = prop_dp_list
                else:
                    # not present in proposal and present in pdp_info, print as existing info
                    check_nd_print(pdp_bp, pdp_bp, "\t-")
                    check_nd_print("", "", " \t  " + common.DEPENDS_KEY + ":")
                    for pdp_dp in pdp_bp_info.bin_pkg_depends:
                        check_nd_print(pdp_dp, pdp_dp, "\t\t-")
                    bp_data.bin_pkg_depends = pdp_bp_info.bin_pkg_depends
                final_bin_pkg_dict[pdp_bp] = bp_data

            for prop_bp, prop_bp_info in prop_src_pkg_info.bin_pkg_dict.items():
                # checking the remaining binary packages present in proposal file
                if prop_bp not in pdp_src_pkg_info.bin_pkg_data_dict:
                    # if not present, then it adding package information
                    bp_data = common.PDPInfo.BinPkgData()
                    bp_data.bin_pkg_depends = prop_src_pkg_info.bin_pkg_dict[prop_bp]
                    final_bin_pkg_dict[prop_bp] = bp_data
                    check_nd_print("", prop_bp, "\t-")
                    check_nd_print("", " ", "\t  " + common.DEPENDS_KEY + ":")
                    for prop_dp in prop_src_pkg_info.bin_pkg_dict[prop_bp]:
                        check_nd_print("", prop_dp, "\t\t-")

            # modify in_target value in pkglist only when proposal value says 'in_target=True', otherwise don't change
            if prop_src_pkg_info.in_target == 'True':
                final_in_target = prop_src_pkg_info.in_target
                final_in_cve = prop_src_pkg_info.n_cve
            else:
                final_in_target = pdp_src_pkg_info.in_target
                final_in_cve = pdp_src_pkg_info.n_cve

            check_nd_print(pdp_src_pkg_info.in_target, final_in_target, " " + common.IN_TARGET_KEY + ": ")

            # re-evaluate the security criteria with final binary package list
            if final_in_target == 'True':
                final_security_criteria = common.evaluate_security_criteria(apt, final_bin_pkg_dict.keys())
            else:
                final_security_criteria = pdp_src_pkg_info.security_criteria
            check_nd_print(pdp_src_pkg_info.security_criteria, final_security_criteria,
                           " " + common.SECURITY_CRITERIA_KEY + ": ")

            check_nd_print(pdp_src_pkg_info.n_cve, final_in_cve, " " + common.N_CVE_KEY + ": ")

            # append the proposal reason to the existing pkglist reason
            if prop_src_pkg_info.reason in pdp_src_pkg_info.reason:
                final_reason = pdp_src_pkg_info.reason
            else:
                final_reason = pdp_src_pkg_info.reason + '\n' + prop_src_pkg_info.reason
            check_nd_print(pdp_src_pkg_info.reason, final_reason, " " + common.REASON_KEY + ": ")

            # update the source package information to PDP information
            pdp_src_pkg_info.src_pkg_name = sp
            if proposal_info.proposer_name not in pdp_src_pkg_info.proposer:
                pdp_src_pkg_info.proposer.append(proposal_info.proposer_name)
            pdp_src_pkg_info.pdp_version = proposal_info.pdp_revision
            pdp_src_pkg_info.in_target = final_in_target
            pdp_src_pkg_info.security_criteria = final_security_criteria
            pdp_src_pkg_info.n_cve = prop_src_pkg_info.n_cve
            pdp_src_pkg_info.reason = final_reason
            pdp_src_pkg_info.bin_pkg_data_dict = final_bin_pkg_dict
            pdp_info_obj.update_src_pkg_info(pdp_src_pkg_info)
        else:
            # if not present, then this is a new package adding to the pdp_info file
            check_nd_print("", sp, "")
            check_nd_print("", proposal_info.proposer_name, " " + common.PROPOSER_KEY + ": ")
            check_nd_print("", proposal_info.pdp_revision, " " + common.PDP_REVISION_KEY + ": ")
            check_nd_print("", " ", " " + common.BIN_PKGS_KEY + ": ")
            for prop_bp, prop_bp_dep in prop_src_pkg_info.bin_pkg_dict.items():
                check_nd_print("", prop_bp, "\t-")
                check_nd_print("", " ", "\t  " + common.DEPENDS_KEY + ":")
                for prop_dp in prop_bp_dep:
                    check_nd_print("", prop_dp, "\t\t-")
            check_nd_print("", prop_src_pkg_info.in_target, " " + common.IN_TARGET_KEY + ": ")
            check_nd_print("", prop_src_pkg_info.security_criteria, " " + common.SECURITY_CRITERIA_KEY + ": ")
            check_nd_print("", prop_src_pkg_info.n_cve, " " + common.N_CVE_KEY + ": ")
            check_nd_print("", prop_src_pkg_info.reason, " " + common.REASON_KEY + ": ")

            # Add as a new source package information to pdp_info_obj
            pdp_pkg_data = common.PDPInfo.SrcPkgData()
            pdp_pkg_data.src_pkg_name = sp
            pdp_pkg_data.proposer = [proposal_info.proposer_name]
            pdp_pkg_data.pdp_version = proposal_info.pdp_revision
            pdp_pkg_data.in_target = prop_src_pkg_info.in_target
            pdp_pkg_data.security_criteria = prop_src_pkg_info.security_criteria
            pdp_pkg_data.n_cve = prop_src_pkg_info.n_cve
            pdp_pkg_data.reason = prop_src_pkg_info.reason
            for bp, bp_info in prop_src_pkg_info.bin_pkg_dict.items():
                bin_pkg_data = common.PDPInfo.BinPkgData()
                bin_pkg_data.bin_pkg_depends = bp_info
                pdp_pkg_data.bin_pkg_data_dict[bp] = bin_pkg_data
            pdp_info_obj.set_src_pkg_info(pdp_pkg_data)


def add_proposal_data(proposal_file):
    """
    add proposal information to pdp_info file
    :param proposal_file: proposal file path
    :return: None
    """
    proposal_info = common.PDPProposal().load(proposal_file)

    if proposal_info:
        pdp_info_obj = common.PDPInfo(proposal_info.proposed_debian_version)
        pdp_info_obj.load_pdp()

        apt = common.Apt()
        if not apt.apt_initialize(proposal_info.proposed_debian_version):
            common.die("Apt initialize is failed")

        # check if there are any unmet dependencies in the proposal file
        unmet_dep_dic = check_deps.get_unmet_dep(apt, pdp_info_obj, proposal_info)
        if unmet_dep_dic:
            print("There are unmet dependencies, Please re-create the proposal with all the dependencies included.")
            check_deps.display_unmet_dep(unmet_dep_dic)
            exit(1)

        validate_proposal(apt, pdp_info_obj, proposal_info)
        if common.input_choose_radio2(Q1, ['Yes', 'No']) == 'No':
            return
        else:
            pdp_info_obj.dump_pdp()
        print("Done")
    else:
        print("Failed to load proposal file")


def usage():
    print("Usage:  pdp_helper <command> <codename> [options] pkg1 [pkg2 ...]")
    print("        pdp_helper add-proposal <proposal file>")
    print("\npdp_helper script to add (or) remove (or) show information to / from pkglist_<codename>.yml file\n")
    print("Available commands:")
    print("\t add - add package information to PDP file")
    print("\t remove - remove package information from PDP file ")
    print("\t show - show package information from PDP file")
    print("\nAvailable options:")
    print("\t --srcpkg specifies the input packages are source packages")
    exit(1)


def parse_options(args):
    global is_src_pkg

    parser = optparse.OptionParser()
    parser.add_option('--srcpkg',
                      dest="isSrcPkg",
                      default=False,
                      action="store_true",
                      help="Indicates the given input package is source packages"
                      )

    options, remainder = parser.parse_args(args)
    is_src_pkg = options.isSrcPkg
    return remainder


def main(argv):
    global is_src_pkg
    apt = common.Apt()

    try:
        if len(argv) <= 1:
            usage()

        if argv[0] == "add-proposal":
            add_proposal_data(argv[1])
            exit(1)

        if len(argv) <= 2:
            usage()

        if argv[0] in ["add", "remove", "show"]:
            command = argv[0]
        else:
            usage()
        if argv[1] in common.DEBIAN_CODE_NAMES:
            codename = argv[1]
        else:
            usage()

        reminder = parse_options(argv[2:])
        if len(reminder) <= 0:
            usage()

        pdp_info = common.PDPInfo(codename)
        res = pdp_info.load_pdp()

        if command == "add":
            if not apt.apt_initialize(codename):
                common.die("Apt initialize is failed")

            gpd.load_prv_sel_pkg_list(pdp_info)
            if is_src_pkg:
                add_src_pkg(pdp_info, reminder, apt)
            else:
                add_bin_pkg(pdp_info, reminder, apt)

        elif command == "remove":
            if not res:
                common.die("No Information in the package list")

            if is_src_pkg:
                remove_src_pkg(pdp_info, reminder)
            else:
                remove_bin_pkg(pdp_info, reminder)

        elif command == "show":
            if not res:
                common.die("No Information in the package list")

            if is_src_pkg:
                show_src_pkg(pdp_info, reminder)
            else:
                show_bin_pkg(pdp_info, reminder)

        elif command in ("help", "--help", "-h"):
            usage()
        else:
            usage()

    # except Exception as e:
    #    print("Exception:")
    finally:
        del apt


if __name__ == "__main__":
    main(sys.argv[1:])
